(ns jumpluff.poi-message
  (:require [cljs.core.async :as async]
            [clojure.string :as s]
            [jumpluff.state :as state]
            [jumpluff.utils :refer [log
                                    promise-to-chan
                                    server-emoji-id]])
  (:require-macros [cljs.core.async.macros :as async-macros]))

(def discord (js/require "discord.js"))

(def ^:dynamic *page-size* 4)
(def ^:dynamic *msg-map-size* 6)
(def ^:dynamic *enable-reactions* true)

(defn- gps-to-keyless-staticmap-thumbnail [gps]
  (str "https://maps.googleapis.com/maps/api/staticmap?center="
       gps "&zoom=12&size=80x80&markers=size:small%7Ccolor:0xff0000%7Clabel:%7C" gps))

(defn osm [gps zoom tile-server]
  (let [[lat long] (map js/Number (s/split gps #"," 2))
        long2tile (fn [lon zoom]
                    (-> lon
                       (+ 180)
                       (/ 360)
                       (* (.pow js/Math 2 zoom))
                       js/Math.floor))
        lat2tile (fn [lat zoom]
                   (-> (as-> (-> lat
                               (* js/Math.PI)
                               (/ 180))
                          <>
                        (-> (/ 1 (.cos js/Math <>))
                           (+ (.tan js/Math <>))))
                      js/Math.log
                      (/ js/Math.PI)
                      (as-> <> (- 1 <>))
                      (/ 2)
                      (* (.pow js/Math 2 zoom))
                      js/Math.floor))]
    (str tile-server zoom "/"
         (long2tile long zoom) "/"
         (lat2tile lat zoom) ".png")))

(defn thumbnail [gps]
  ;; (gps-to-keyless-staticmap-thumbnail gps)
  (when-let [tiles (not-empty js/process.env.OSM_TILE_SERVER)]
    (osm gps 16 tiles)))

(defn- gps-to-maps-link [gps]
  (str "https://maps.google.com/?daddr=" gps))

(defn- poi-format-name [poi source-message]
  (str (:name poi)
       (when-let [ex (:ex poi)]
         (str " "
              (or (when source-message
                    (get @state/server-emoji
                         (server-emoji-id source-message "ex")))
                  "(EX)")
              (when (= ex :ex-triggered)
                ":tada:")))))

(defn- poi-format-description [poi]
  (str "["
       (or (not-empty (:description poi))
           (:gps poi))
       "](" (gps-to-maps-link (:gps poi)) ")"))

(def msg-map (atom {}))

(defn- add-to-ring
  "`swap! assoc` into `ring`, when count of `ring` reaches `limit`
  `dissoc` the `first`.  Just before dissoc run `cleanup-fn` on the
  trimmed value.  `assoc` adds to the tail of the array-map, so first
  is assumed to be the oldest item. Return `val`."
  [ring limit id val cleanup-fn]
  (when (< (dec limit)
           (count @ring))
    (when-let [[head-id head-val] (first @ring)]
      (cleanup-fn head-val)
      (swap! ring dissoc head-id)))
  (swap! ring assoc id val)
  val)

(defn poi-collection-and-src-msg-to-obj
  "Create and add the msg-map context for `collection` and
  `source-message`."
  [collection source-message]
  (add-to-ring msg-map *msg-map-size*
               source-message.id
               {:collection collection
                :msg-source source-message
                :msg-sent (atom nil)
                :page (atom 0)
                :count-pages (atom 0)
                :count-current-page (atom 0)
                :reaction-collector (atom nil)
                :reaction-in-progress (atom false)
                :presupply-in-progress (atom false)
                :presupply-stop-p (atom false)}
               #(some-> @(:reaction-collector %)
                        (.stop "time"))))

(defn poi-collection-to-message
  "Create a RichEmbed from a msg-map context map."
  ([m]
   (poi-collection-to-message m 0 nil))
  ([m page-n narrow-to-nth]
   (let [embed (discord.RichEmbed.)
         collection-pages (partition *page-size* *page-size* nil (:collection m))
         count-pages (count collection-pages)
         page-number (or (and (int? page-n)
                              (< 0 page-n)
                              (< page-n count-pages)
                              page-n) 0)
         narrow (and (int? narrow-to-nth)
                     (some #{narrow-to-nth} (range *page-size*)))
         current-page (let [p (nth collection-pages page-number [])]
                        (if (and narrow (< narrow (count p)))
                          (list (nth p narrow []))
                          p))
         count-current-page (count current-page)]
     (do (reset! (:page m) (if narrow 0 page-number))
         (reset! (:count-pages m) (if narrow 1 count-pages))
         (reset! (:count-current-page m) count-current-page))
     (when (<= 1 count-current-page)
       (aset embed "fields"
             (clj->js
              (map (fn [poi] {:name (poi-format-name poi (:msg-source m))
                             :value (poi-format-description poi)
                             :inline false})
                   current-page))))
     (when (= 1 count-current-page)
       (let [gps (:gps (first current-page))]
         (.setThumbnail embed (thumbnail gps))
         (.setURL embed (gps-to-maps-link gps))))
     (when (and (not narrow)
                (not= 1 count-pages))
       (.setFooter embed
                   (cond (< 1 count-pages)
                         (str "page "
                              (inc page-number)
                              " out of "
                              count-pages
                              " pages, "
                              (count (:collection m))
                              " results")
                         (zero? count-current-page)
                         "No results found.")))
     embed)))

(def static-reaction-emoji-map
  {"green-done" "✅"
   "del" "🇽"
   "prev" "⬅"
   "digit-1" "1⃣"
   "digit-2" "2⃣"
   "digit-3" "3⃣"
   "digit-4" "4⃣"
   "next" "➡"})

(defn- poimsg-context-reaction-predicates
  "Evaluate if `emoji-name` should be applied given the context map."
  [m emoji-name]
  (case emoji-name
    "digit-1" (< 1 @(:count-current-page m))
    "digit-2" (<= 2 @(:count-current-page m))
    "digit-3" (<= 3 @(:count-current-page m))
    "digit-4" (<= 4 @(:count-current-page m))
    "prev" (< 0 @(:page m))
    "next" (< (inc @(:page m)) ; zero based, vs starts at one
              @(:count-pages m))
    true))

(defn- clear-and-stop-reactions
  "Wrap `.clearReactions`.
  If a presupply reactions call was in progress, interrupt and wait for it.
  Then make the clearReactions call and let the go-block finish."
  [m]
  (async-macros/go
    (when-let [in-progress @(:presupply-in-progress m)]
      (reset! (:presupply-stop-p m) true) ; interrupt presupply
      (async/<! in-progress)) ; wait for presupply to finish
    (some->> @(:msg-sent m)
             (promise-to-chan #(.clearReactions %))
             async/<!)))

(defn- reaction-menu-poi-presupply
  "Add the applicable initial reactions to a point of interest response.
  The discord api does not support multiple reactions being added in a
  bulk call, so this will be a slow process adding one by one.  The
  atom `:presupply-in-progress` will hold the go-block channel when a
  call in progress, reset to false when complete.  To interrupt this
  process, set `:presupply-stop-p` to true and the go-block will
  finish after the call it was making."
  [m]
  (swap! (:presupply-in-progress m)
         (fn [current-value]
           (async-macros/go
             ;; wait for the last run
             (when current-value
               (log "waiting for last run of presupply to wrap up")
               (async/<! current-value))
             (when-let [msg @(:msg-sent m)]
               (let [stopped (atom false)]
                 (doseq [emoji-key (keys static-reaction-emoji-map)]
                   (when (and (not @(:presupply-stop-p m))
                              (poimsg-context-reaction-predicates m emoji-key))
                     (when (not (.-deleted msg))
                       (->> emoji-key
                          (get static-reaction-emoji-map)
                          (promise-to-chan #(.react msg %))
                          async/<!)))
                   (when (and @(:presupply-stop-p m)
                              (not (.-deleted msg))
                              (not @stopped))
                     (reset! stopped true)))) ; hack to stop the doseq, using doseq for <!
               (reset! (:presupply-stop-p m) false)
               (reset! (:presupply-in-progress m) false))))))

(defn- poi-handle-reactions
  "Handle the reaction added event for a msg-map context map"
  [m]
  (fn [reaction]
    (log "—Sent message, "
         [(.-id @(:msg-sent m)) (.-id (:msg-source m))]
         " received: "  reaction.emoji.name)
    (when (some #{(.-author.id (:msg-source m))}
                (map #(.-id %) (.array reaction.users)))
      (swap! (:reaction-in-progress m)
             (fn [current-value]
               (async-macros/go
                 ;; wait for the last run
                 (when current-value
                   (log "waiting for last run")
                   (async/<! current-value))
                 ;; hold up here until finished
                 (cond (= reaction.emoji.name
                          (get static-reaction-emoji-map "green-done"))
                       (do (some-> @(:reaction-collector m)
                                   (.stop "checkmark reaction"))
                           (async/<! (clear-and-stop-reactions m)))
                       (= reaction.emoji.name
                          (get static-reaction-emoji-map "del"))
                       (do (reset! (:msg-sent m) nil) ; dissoc this as the message will be deleted
                           (async/<! (promise-to-chan (.delete reaction.message)))
                           (some-> @(:reaction-collector m)
                                   (.stop "marked for deletion"))
                           (log "—Deleted message")))
                 (doseq [[emoji-name page-n narrow-to-nth]
                         (let [page-n @(:page m)]
                           [["digit-1" page-n 0]
                            ["digit-2" page-n 1]
                            ["digit-3" page-n 2]
                            ["digit-4" page-n 3]
                            ["prev" (dec page-n) nil]
                            ["next" (inc page-n) nil]])]
                   (when (and (= reaction.emoji.name
                                 (get static-reaction-emoji-map emoji-name))
                              (poimsg-context-reaction-predicates m emoji-name))
                     (->> (poi-collection-to-message m page-n narrow-to-nth)
                        (promise-to-chan #(.edit reaction.message %))
                        async/<!)
                     (async/<! (clear-and-stop-reactions m))
                     (async/<! (reaction-menu-poi-presupply m))))
                 (reset! (:reaction-in-progress m) false)))))))

(defn- start-reaction-collector
  [message filter timeout end-cb collect-cb]
  (doto (.createReactionCollector message filter
                                  (clj->js {:time timeout}))
    (.on "end" end-cb)
    (.on "collect" collect-cb)))

(defn poi-once-sent
  "Start the reaction collector on sent responses to `command-name`"
  [sent-message src-message]
  (when-let [m (get @msg-map src-message.id nil)]
    (reset! (:msg-sent m) sent-message)
    (log "—once sent ids:" [sent-message.id src-message.id m])
    (reaction-menu-poi-presupply m)
    (->> (start-reaction-collector @(:msg-sent m)
                                 (fn [reaction user] (not= user.id (.-user.id @state/client)))
                                 (* 3 60 1000) ; min to ms
                                 (fn [collected reason]
                                   (log "—end reaction collector reason: " reason)
                                   (when (= reason "time")
                                     (clear-and-stop-reactions m))
                                   (swap! msg-map dissoc src-message.id))
                                 (poi-handle-reactions m))
       (reset! (:reaction-collector m)))))

(defn cleanup-msg-map
  []
  (async-macros/go
    (doseq [m (vals @msg-map)]
      (async/<! (clear-and-stop-reactions m)))))
